#!/usr/bin/python2.4
#
# Copyright 2009, Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
#     * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
#     * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

"""Reconciles offline work for p4.

p4reconcile c:\depot\myclientdir\somefolder

will create a changelist with edit, add and deletes to make the
correpsonding folder in the depot match the somefolder and everything below it.

It's up to you to actually submit the changelist. In other words, no
real work happens except for the creation of a changelist."""


from optparse import OptionParser
import os
import os.path
import sys
import p4manager


def GetTreeFiles(local_path):
  """Returns a list of files including all subfolders in a folder tree.

  Args:
    local_path: Path to get list from.

  Returns:
    list of files from local_path.
  """
  file_list = []
  for root, _, files in os.walk(local_path):
    for name in files:
      file_list.append(os.path.join(root, name))
  return file_list


def FilterDepotFiles(depot_files, remove_actions):
  """Removes any files with certain actions.

  Args:
    depot_files: Files from depot as retuned from GetDepotFiles
    remove_actions: list of actions to remove.

  Returns:
    An list of dicts in the same format as returned by GetDepotFiles but with
    the files that have the specific actions removed.
  """
  new_files = []
  for record in depot_files:
    if not record["action"] in remove_actions:
      new_files.append(record)
  return new_files


def RelativizePath(path, source):
  """Returns a path relative to source. Backslashes will be changed to slashes.

  Usage example:
  RelativePath("a/b/c/d", "a/b/e/f") = "../../c/d"

  Basically, if your current working directory is source what path would you
  need to type to access 'path'.

  Needed so we run in python 2.4 :-(

  Args:
    path: Path to make relative.
    source: Path to make path relative to.

  Returns:
    path relative to source.
  """
  path = os.path.abspath(path).replace("\\", "/")
  path_parts = path.split("/")
  source = os.path.normpath(source).replace("\\", "/")
  source_parts = source.split("/")
  ii = 0
  while ii < len(path_parts) and ii < len(source_parts):
    if not path_parts[ii] == source_parts[ii]:
      break
    ii += 1

  return os.path.join("../" * len(source_parts[ii:]),
                      "/".join(path_parts[ii:])).replace("\\", "/")


def ReconcileOfflineWork(local_path, execute, verbose):
  """Reconciles a local folder with the corresponding p4 depot folder.

  Args:
    local_path: path on local machine. Must be in your clientspec.
    execute: True to actually issue p4 commands, False to just print them.
    verbose: True to print extra info.
  """
  p4_manager = p4manager.P4Manager(execute, verbose)

  local_path = os.path.abspath(local_path)
  depot_path = p4_manager.GetDepotPathForFolder(local_path)

  depot_files = p4_manager.GetDepotFiles(depot_path)
  local_files = GetTreeFiles(local_path)
  depot_files = FilterDepotFiles(depot_files, ["delete"])

  # convert depot_files to a dict for easier lookup
  depot_file_map = {}
  for record in depot_files:
    filename = RelativizePath(record["depotFile"], depot_path)
    depot_file_map[filename] = record["action"]

  changes = []
  additions = []

  # go through local_path.
  for local_file in local_files:
    local_file = RelativizePath(local_file, local_path)
    # if it's in the depot_file_map
    if local_file in depot_file_map:
      # add it to changes
      changes.append(local_file)
      # remove it from the map
      del depot_file_map[local_file]
    else:
      # otherwise add it to additions
      additions.append(local_file)

  changelist = p4_manager.CreateChangelist("Reconcile offline changes for: " +
                                           local_path)

  for filename in additions:
    changelist.Add(os.path.join(local_path, filename))

  for filename in changes:
    changelist.Edit(os.path.join(local_path, filename))

  for filename in depot_file_map:
    changelist.Delete(os.path.join(local_path, filename))

  changelist.RevertUnchanged()

  print "created changelist:", changelist.changelist


def main():
  """Calls ReconcileOfflineWork with command line arguments.

  run with -v -n to see what p4 commands __would__ be executed.
  run with just -v to see the commands AS they are executed.
  """
  parser = OptionParser(usage="usage: %prog [options] local_path")
  parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
                    default=False,
                    help="print extra info.")
  parser.add_option("-n", "--no-execute", action="store_false", dest="execute",
                    default=True,
                    help="don't execute commands, just print them.")

  (options, args) = parser.parse_args()
  if len(args) != 1:
    parser.print_help()
    sys.exit(1)

  local_folder = args[0]

  ReconcileOfflineWork(local_folder, options.execute, options.verbose)


if __name__ == "__main__":
  main()

